Skip to content

feat: [GH-140] Add *AndEdit atomic read-modify-write methods#143

Open
javierlores wants to merge 2 commits into
feature/GH-139from
feature/GH-140
Open

feat: [GH-140] Add *AndEdit atomic read-modify-write methods#143
javierlores wants to merge 2 commits into
feature/GH-139from
feature/GH-140

Conversation

@javierlores

Copy link
Copy Markdown
Contributor

Closes #140. Stacked on #141 (GH-139 findFirst) — base is feature/GH-139 so this diff shows only the *AndEdit delta. Retarget to develop once #141 merges.

What this does

Adds the *AndEdit family of atomic read-modify-write primitives to Runway:

  • findAndEdit(Class<T>, Criteria, Consumer<T>) — edits every match; all edits commit in one transaction (all-or-nothing). Empty set when no match (consumer never runs).
  • findUniqueAndEdit(Class<T>, Criteria, Consumer<T>) — edits the single match; null when none; throws DuplicateEntryException on >1 (checked inside the txn, before the consumer runs).
  • findFirstAndEdit(Class<T>, Criteria, Order, Consumer<T>) — edits the first match under the required Order; null when none.

All three route through a private editWithinTransaction(...) helper.

How it works (design A from the ticket)

True single-transaction read-modify-write: an IncrementalReader and an IncrementalSaver are bound to the same staged Concourse connection, so the find's read and the save's write share one transaction and one set of locks. That shared lock set is what gives concurrent callers real mutual exclusion (the connector-claiming use case in cinchapi-server).

  • Uses IncrementalSaver unconditionally (even when supportsBulkCommands), because BatchSaver defers the server-side STAGE to commit time, which would place the find's read outside the transaction. Documented inline.
  • On TransactionException: abort and retry the whole cycle (re-find → re-apply → re-save) up to MAX_SPURIOUS_SAVE_RETRIES, with jittered exponential backoff (backoffWithJitter). Each retry re-finds fresh instances.
  • On budget exhaustion: throws the new RetryExhaustedException (carries the attempt count) rather than returning a non-committed result — distinct from null/empty (= no match).
  • Non-transaction failures (e.g. duplicate/constraint) are terminal: abort and propagate, no retry.

Files

  • Runway.java — three public methods + editWithinTransaction + backoffWithJitter (+ RETRY_BACKOFF_BASE_MILLIS).
  • RetryExhaustedException.java — new, extends RunwayException.
  • FindAndEditTest, FindUniqueAndEditTest, FindFirstAndEditTest — parameterized over BatchSaver/IncrementalSaver, including concurrency tests.

Testing

Tests written but not run, per repo conventions (live Concourse server required). spotlessApply run for formatting.

Add findAndEdit, findUniqueAndEdit, and findFirstAndEdit to Runway as
true single-transaction read-modify-write primitives: each binds a
Reader and Saver to one staged connection so the find's read and the
save's write share one transaction (and one set of locks), giving
concurrent callers mutual exclusion. On write conflict the cycle is
retried with jittered backoff up to the bounded attempt limit; on
exhaustion a RetryExhaustedException is thrown so a contended claim is
never mistaken for 'nothing matched'.

The incremental path is used unconditionally because BatchSaver defers
the server-side STAGE until commit time, which would place the read
outside the transaction.

Stacked on feature/GH-139 (findFirst).

Tests written (not run) per repo policy; spotlessApply clean.
Apply Order/Page client-side in editWithinTransaction when the connected
server cannot sort or paginate natively, mirroring the read-path
fallback. The in-transaction find still reads (and therefore locks)
every match, so atomicity and mutual exclusion are preserved; only the
ordering/limit are applied client-side afterward via a new sortAndPage
helper. This keeps findFirstAndEdit and the paginated guards correct on
legacy servers, not just native-capable ones.

Also:
- Drop the unused `any` (hierarchy) parameter from editWithinTransaction;
  every call site passed false.
- Honor interrupts during the retry backoff: restore the interrupt flag
  and abandon the retry loop instead of silently continuing to contend.
- Add NOTE comments documenting the retry-on-every-TransactionException
  semantics and the connection-held-across-backoff choice.

Tests: legacy-path coverage (hasNativeSortingAndPagination=false) for
findFirstAndEdit client-side ordering and findUniqueAndEdit duplicate
detection without native pagination.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant